/*******************************************************************
** This code is part of Breakout.
**
** Breakout is free software: you can redistribute it and/or modify
** it under the terms of the CC BY 4.0 license as published by
** Creative Commons, either version 4 of the License, or (at your
** option) any later version.
******************************************************************/
#include <algorithm>
#include <iostream>
#include <sstream>

#include <irrklang/irrKlang.h>
using namespace irrklang;

#include "ball_object.h"
#include "game.h"
#include "game_object.h"
#include "particle_generator.h"
#include "post_processor.h"
#include "resource_manager.h"
#include "sprite_renderer.h"
#include "text_renderer.h"

// Game-related State data
SpriteRenderer *Renderer;
GameObject *Player;
BallObject *Ball;
ParticleGenerator *Particles;
PostProcessor *Effects;
ISoundEngine *SoundEngine = createIrrKlangDevice();
TextRenderer *Text;

float ShakeTime = 0.0f;

Game::Game(unsigned int width, unsigned int height)
    : State(GAME_MENU), Keys(), KeysProcessed(), Width(width), Height(height),
      Lives(3) {}

Game::~Game() {
  delete Renderer;
  delete Player;
  delete Ball;
  delete Particles;
  delete Effects;
  delete Text;
  SoundEngine->drop();
}

void Game::Init() {
  // load shaders
  ResourceManager::LoadShader("resources/shaders/sprite.vert",
                              "resources/shaders/sprite.frag", nullptr,
                              "sprite");
  ResourceManager::LoadShader("resources/shaders/particle.vert",
                              "resources/shaders/particle.frag", nullptr,
                              "particle");
  ResourceManager::LoadShader("resources/shaders/post_processing.vert",
                              "resources/shaders/post_processing.frag", nullptr,
                              "postprocessing");
  // configure shaders
  glm::mat4 projection =
      glm::ortho(0.0f, static_cast<float>(this->Width),
                 static_cast<float>(this->Height), 0.0f, -1.0f, 1.0f);
  ResourceManager::GetShader("sprite").Use().SetInteger("sprite", 0);
  ResourceManager::GetShader("sprite").SetMatrix4("projection", projection);
  ResourceManager::GetShader("particle").Use().SetInteger("sprite", 0);
  ResourceManager::GetShader("particle").SetMatrix4("projection", projection);
  // load textures
  ResourceManager::LoadTexture("resources/textures/background.png", true,
                               "background");
  ResourceManager::LoadTexture("resources/textures/ball.png", true, "face");
  ResourceManager::LoadTexture("resources/textures/block.png", true, "block");
  ResourceManager::LoadTexture("resources/textures/block_solid.png", true,
                               "block_solid");
  ResourceManager::LoadTexture("resources/textures/paddle.png", true, "paddle");
  ResourceManager::LoadTexture("resources/textures/particle.png", true,
                               "particle");
  ResourceManager::LoadTexture("resources/textures/powerup_speed.png", true,
                               "powerup_speed");
  ResourceManager::LoadTexture("resources/textures/powerup_sticky.png", true,
                               "powerup_sticky");
  ResourceManager::LoadTexture("resources/textures/powerup_increase.png", true,
                               "powerup_increase");
  ResourceManager::LoadTexture("resources/textures/powerup_confuse.png", true,
                               "powerup_confuse");
  ResourceManager::LoadTexture("resources/textures/powerup_chaos.png", true,
                               "powerup_chaos");
  ResourceManager::LoadTexture("resources/textures/powerup_passthrough.png",
                               true, "powerup_passthrough");
  // set render-specific controls
  Renderer = new SpriteRenderer(ResourceManager::GetShader("sprite"));
  Particles =
      new ParticleGenerator(ResourceManager::GetShader("particle"),
                            ResourceManager::GetTexture("particle"), 500);
  Effects = new PostProcessor(ResourceManager::GetShader("postprocessing"),
                              this->Width, this->Height);
  Text = new TextRenderer(this->Width, this->Height);
  Text->Load("resources/fonts/OCRAEXT.TTF", 24);
  this->Level = new GameLevel(this->Width, this->Height / 2);
  // configure game objects
  glm::vec2 playerPos = glm::vec2(this->Width / 2.0f - PLAYER_SIZE.x / 2.0f,
                                  this->Height - PLAYER_SIZE.y);
  Player = new GameObject(playerPos, PLAYER_SIZE,
                          ResourceManager::GetTexture("paddle"));
  glm::vec2 ballPos = playerPos + glm::vec2(PLAYER_SIZE.x / 2.0f - BALL_RADIUS,
                                            -BALL_RADIUS * 2.0f);
  Ball = new BallObject(ballPos, BALL_RADIUS, INITIAL_BALL_VELOCITY,
                        ResourceManager::GetTexture("face"));
  // audio
  SoundEngine->play2D("resources/audio/breakout.mp3", true);
}

void Game::Update(float dt) {
  // update objects
  Ball->Move(dt, this->Width);
  // check for collisions
  this->DoCollisions();
  // update particles
  Particles->Update(dt, *Ball, 2, glm::vec2(Ball->Radius / 2.0f));
  // update PowerUps
  this->UpdatePowerUps(dt);
  // reduce shake time
  if (ShakeTime > 0.0f) {
    ShakeTime -= dt;
    if (ShakeTime <= 0.0f)
      Effects->Shake = false;
  }
  // check loss condition
  if (Ball->Position.y >= this->Height) // did ball reach bottom edge?
  {
    --this->Lives;
    // did the player lose all his lives? : game over
    if (this->Lives == 0) {
      this->ResetLevel();
      this->State = GAME_MENU;
    }
    this->ResetPlayer();
  }
  // check win condition
  if (this->State == GAME_ACTIVE && this->Level->IsCompleted()) {
    this->ResetLevel();
    this->ResetPlayer();
    Effects->Chaos = true;
    this->State = GAME_WIN;
  }
}

void Game::ProcessInput(float dt) {
  if (this->State == GAME_MENU) {
    if (this->Keys[GLFW_KEY_ENTER] && !this->KeysProcessed[GLFW_KEY_ENTER]) {
      this->State = GAME_ACTIVE;
      this->KeysProcessed[GLFW_KEY_ENTER] = true;
    }
  }
  if (this->State == GAME_WIN) {
    if (this->Keys[GLFW_KEY_ENTER]) {
      this->KeysProcessed[GLFW_KEY_ENTER] = true;
      Effects->Chaos = false;
      this->State = GAME_MENU;
    }
  }
  if (this->State == GAME_ACTIVE) {
    float velocity = PLAYER_VELOCITY * dt;
    // move playerboard
    if (this->Keys[GLFW_KEY_A]) {
      if (Player->Position.x >= 0.0f) {
        Player->Position.x -= velocity;
        if (Ball->Stuck)
          Ball->Position.x -= velocity;
      }
    }
    if (this->Keys[GLFW_KEY_D]) {
      if (Player->Position.x <= this->Width - Player->Size.x) {
        Player->Position.x += velocity;
        if (Ball->Stuck)
          Ball->Position.x += velocity;
      }
    }
    if (this->Keys[GLFW_KEY_SPACE])
      Ball->Stuck = false;
  }
}

void Game::DrawFlag() {
  Text->RenderText("V", 5.0f, 35.0f, 1.0f);
  Text->RenderText("N", 25.0f, 35.0f, 1.0f);
  Text->RenderText("C", 150.0f, 30.0f, 1.8f);
  Text->RenderText("T", 498.0f, 22.0f, 2.0f);
  Text->RenderText("F", 700.0f, 10.0f, 2.0f);
  Text->RenderText("{", 7.0f, 80.0f, 5.0f);
  Text->RenderText("str1ngs_1s_silver_bullet?", 200.0f, 23.0f, 0.2f);
  Text->RenderText("}", 746.0f, 170.0f, 5.0f);
  Text->RenderText("3", 315.0f, 106.0f, 1.2f);
  Text->RenderText("4", 456.0f, 77.0f, 3.6f);
  Text->RenderText("4", 223.0f, 177.0f, 1.8f);
  Text->RenderText("0", 256.0f, 169.0f, 1.5f);
  Text->RenderText("0", 344.0f, 188.0f, 1.7f);
  Text->RenderText("5", 498.0f, 82.0f, 2.4f);
  Text->RenderText("5", 283.0f, 182.0f, 2.4f);
  Text->RenderText("u", 256.0f, 86.0f, 1.0f);
  Text->RenderText("V_", 64.0f, 173.0f, 1.5f);
  Text->RenderText("r", 288.0f, 98.0f, 1.0f);
  Text->RenderText("r", 643.0f, 116.0f, 1.4f);
  Text->RenderText("3", 32.0f, 179.0f, 1.7f);
  Text->RenderText("r", 9.0f, 173.0f, 2.0f);
  Text->RenderText("_", 333.0f, 106.0f, 1.2f);
  Text->RenderText("G00dCh34t3r", 878.0f, 60.0f, 1.4f);
  Text->RenderText("_8703d0ccfef0", 400.0f, 226.0f, 1.6f);
  Text->RenderText("3", 544.0f, 36.0f, 7.2f);
  Text->RenderText("3", 106.0f, 122.0f, 1.7f);
  Text->RenderText("t", 533.0f, 74.0f, 1.7f);
  Text->RenderText("u", 368.0f, 174.0f, 1.5f);
  Text->RenderText("Txt", 58.0f, 80.0f, 4.7f);
  Text->RenderText("g", 188.0f, 178.0f, 2.3f);
  Text->RenderText("M", 375.0f, 48.0f, 5.6f);
  Text->RenderText("h", 316.0f, 184.0f, 1.7f);
  Text->RenderText("_0r_", 678.0f, 120.0f, 1.4f);
}

void Game::Render() {
  if (this->State == GAME_ACTIVE || this->State == GAME_MENU ||
      this->State == GAME_WIN) {
    // begin rendering to postprocessing framebuffer
    Effects->BeginRender();
    // draw background
    Renderer->DrawSprite(ResourceManager::GetTexture("background"),
                         glm::vec2(0.0f, 0.0f),
                         glm::vec2(this->Width, this->Height), 0.0f);
    this->DrawFlag();
    // draw level
    this->Level->Draw(*Renderer);
    // draw player
    Player->Draw(*Renderer);
    // draw PowerUps
    for (PowerUp &powerUp : this->PowerUps)
      if (!powerUp.Destroyed)
        powerUp.Draw(*Renderer);
    // draw particles
    Particles->Draw();
    // draw ball
    Ball->Draw(*Renderer);
    // end rendering to postprocessing framebuffer
    Effects->EndRender();
    // render postprocessing quad
    Effects->Render(glfwGetTime());
    // render text (don't include in postprocessing)
    std::stringstream ss;
    ss << this->Lives;
    Text->RenderText("Lives:" + ss.str(), 5.0f, 5.0f, 1.0f);
  }
  if (this->State == GAME_MENU) {
    Text->RenderText("Press ENTER to start", 250.0f, this->Height / 2.0f, 1.0f);
  }
  if (this->State == GAME_WIN) {
    Text->RenderText("You WON!!!", 320.0f, this->Height / 2.0f - 20.0f, 1.0f,
                     glm::vec3(0.0f, 1.0f, 0.0f));
    Text->RenderText("Press ENTER to retry or ESC to quit", 130.0f,
                     this->Height / 2.0f, 1.0f, glm::vec3(1.0f, 1.0f, 0.0f));
  }
}

void Game::ResetLevel() {
  delete this->Level;
  this->Level = new GameLevel(this->Width, this->Height / 2);
  this->Lives = 3;
}

void Game::ResetPlayer() {
  // reset player/ball stats
  Player->Size = PLAYER_SIZE;
  Player->Position = glm::vec2(this->Width / 2.0f - PLAYER_SIZE.x / 2.0f,
                               this->Height - PLAYER_SIZE.y);
  Ball->Reset(Player->Position + glm::vec2(PLAYER_SIZE.x / 2.0f - BALL_RADIUS,
                                           -(BALL_RADIUS * 2.0f)),
              INITIAL_BALL_VELOCITY);
  // also disable all active powerups
  Effects->Chaos = Effects->Confuse = false;
  Ball->PassThrough = Ball->Sticky = false;
  Player->Color = glm::vec3(1.0f);
  Ball->Color = glm::vec3(1.0f);
}

// powerups
bool IsOtherPowerUpActive(std::vector<PowerUp> &powerUps, std::string type);

void Game::UpdatePowerUps(float dt) {
  for (PowerUp &powerUp : this->PowerUps) {
    powerUp.Position += powerUp.Velocity * dt;
    if (powerUp.Activated) {
      powerUp.Duration -= dt;

      if (powerUp.Duration <= 0.0f) {
        // remove powerup from list (will later be removed)
        powerUp.Activated = false;
        // deactivate effects
        if (powerUp.Type == "sticky") {
          if (!IsOtherPowerUpActive(
                  this->PowerUps, "sticky")) { // only reset if no other PowerUp
                                               // of type sticky is active
            Ball->Sticky = false;
            Player->Color = glm::vec3(1.0f);
          }
        } else if (powerUp.Type == "pass-through") {
          if (!IsOtherPowerUpActive(
                  this->PowerUps,
                  "pass-through")) { // only reset if no other PowerUp of type
                                     // pass-through is active
            Ball->PassThrough = false;
            Ball->Color = glm::vec3(1.0f);
          }
        } else if (powerUp.Type == "confuse") {
          if (!IsOtherPowerUpActive(
                  this->PowerUps,
                  "confuse")) { // only reset if no other PowerUp of type
                                // confuse is active
            Effects->Confuse = false;
          }
        } else if (powerUp.Type == "chaos") {
          if (!IsOtherPowerUpActive(
                  this->PowerUps, "chaos")) { // only reset if no other PowerUp
                                              // of type chaos is active
            Effects->Chaos = false;
          }
        }
      }
    }
  }
  // Remove all PowerUps from vector that are destroyed AND !activated (thus
  // either off the map or finished) Note we use a lambda expression to remove
  // each PowerUp which is destroyed and not activated
  this->PowerUps.erase(
      std::remove_if(this->PowerUps.begin(), this->PowerUps.end(),
                     [](const PowerUp &powerUp) {
                       return powerUp.Destroyed && !powerUp.Activated;
                     }),
      this->PowerUps.end());
}

bool ShouldSpawn(unsigned int chance) {
  unsigned int random = rand() % chance;
  return random == 0;
}
void Game::SpawnPowerUps(GameObject &block) {
  if (ShouldSpawn(75)) // 1 in 75 chance
    this->PowerUps.push_back(
        PowerUp("speed", glm::vec3(0.5f, 0.5f, 1.0f), 0.0f, block.Position,
                ResourceManager::GetTexture("powerup_speed")));
  if (ShouldSpawn(75))
    this->PowerUps.push_back(
        PowerUp("sticky", glm::vec3(1.0f, 0.5f, 1.0f), 20.0f, block.Position,
                ResourceManager::GetTexture("powerup_sticky")));
  if (ShouldSpawn(75))
    this->PowerUps.push_back(PowerUp(
        "pass-through", glm::vec3(0.5f, 1.0f, 0.5f), 10.0f, block.Position,
        ResourceManager::GetTexture("powerup_passthrough")));
  if (ShouldSpawn(75))
    this->PowerUps.push_back(PowerUp(
        "pad-size-increase", glm::vec3(1.0f, 0.6f, 0.4), 0.0f, block.Position,
        ResourceManager::GetTexture("powerup_increase")));
  if (ShouldSpawn(15)) // Negative powerups should spawn more often
    this->PowerUps.push_back(
        PowerUp("confuse", glm::vec3(1.0f, 0.3f, 0.3f), 15.0f, block.Position,
                ResourceManager::GetTexture("powerup_confuse")));
  if (ShouldSpawn(15))
    this->PowerUps.push_back(
        PowerUp("chaos", glm::vec3(0.9f, 0.25f, 0.25f), 15.0f, block.Position,
                ResourceManager::GetTexture("powerup_chaos")));
}

void ActivatePowerUp(PowerUp &powerUp) {
  if (powerUp.Type == "speed") {
    Ball->Velocity *= 1.2;
  } else if (powerUp.Type == "sticky") {
    Ball->Sticky = true;
    Player->Color = glm::vec3(1.0f, 0.5f, 1.0f);
  } else if (powerUp.Type == "pass-through") {
    Ball->PassThrough = true;
    Ball->Color = glm::vec3(1.0f, 0.5f, 0.5f);
  } else if (powerUp.Type == "pad-size-increase") {
    Player->Size.x += 50;
  } else if (powerUp.Type == "confuse") {
    if (!Effects->Chaos)
      Effects->Confuse = true; // only activate if chaos wasn't already active
  } else if (powerUp.Type == "chaos") {
    if (!Effects->Confuse)
      Effects->Chaos = true;
  }
}

bool IsOtherPowerUpActive(std::vector<PowerUp> &powerUps, std::string type) {
  // Check if another PowerUp of the same type is still active
  // in which case we don't disable its effect (yet)
  for (const PowerUp &powerUp : powerUps) {
    if (powerUp.Activated)
      if (powerUp.Type == type)
        return true;
  }
  return false;
}

// collision detection
bool CheckCollision(GameObject &one, GameObject &two);
Collision CheckCollision(BallObject &one, GameObject &two);
Direction VectorDirection(glm::vec2 closest);

void Game::DoCollisions() {
  for (GameObject &box : this->Level->Bricks) {
    if (!box.Destroyed) {
      Collision collision = CheckCollision(*Ball, box);
      if (std::get<0>(collision)) // if collision is true
      {
        // destroy block if not solid
        if (!box.IsSolid) {
          box.Destroyed = true;
          this->SpawnPowerUps(box);
          SoundEngine->play2D("resources/audio/bleep.mp3", false);
        } else { // if block is solid, enable shake effect
          ShakeTime = 0.05f;
          Effects->Shake = true;
          SoundEngine->play2D("resources/audio/bleep.mp3", false);
        }
        // collision resolution
        Direction dir = std::get<1>(collision);
        glm::vec2 diff_vector = std::get<2>(collision);
        if (!(Ball->PassThrough &&
              !box.IsSolid)) // don't do collision resolution on non-solid
                             // bricks if pass-through is activated
        {
          if (dir == LEFT || dir == RIGHT) // horizontal collision
          {
            Ball->Velocity.x = -Ball->Velocity.x; // reverse horizontal velocity
            // relocate
            float penetration = Ball->Radius - std::abs(diff_vector.x);
            if (dir == LEFT)
              Ball->Position.x += penetration; // move ball to right
            else
              Ball->Position.x -= penetration; // move ball to left;
          } else                               // vertical collision
          {
            Ball->Velocity.y = -Ball->Velocity.y; // reverse vertical velocity
            // relocate
            float penetration = Ball->Radius - std::abs(diff_vector.y);
            if (dir == UP)
              Ball->Position.y -= penetration; // move ball bback up
            else
              Ball->Position.y += penetration; // move ball back down
          }
        }
      }
    }
  }

  // also check collisions on PowerUps and if so, activate them
  for (PowerUp &powerUp : this->PowerUps) {
    if (!powerUp.Destroyed) {
      // first check if powerup passed bottom edge, if so: keep as inactive and
      // destroy
      if (powerUp.Position.y >= this->Height)
        powerUp.Destroyed = true;

      if (CheckCollision(
              *Player, powerUp)) { // collided with player, now activate powerup
        ActivatePowerUp(powerUp);
        powerUp.Destroyed = true;
        powerUp.Activated = true;
        SoundEngine->play2D("resources/audio/powerup.wav", false);
      }
    }
  }

  // and finally check collisions for player pad (unless stuck)
  Collision result = CheckCollision(*Ball, *Player);
  if (!Ball->Stuck && std::get<0>(result)) {
    // check where it hit the board, and change velocity based on where it hit
    // the board
    float centerBoard = Player->Position.x + Player->Size.x / 2.0f;
    float distance = (Ball->Position.x + Ball->Radius) - centerBoard;
    float percentage = distance / (Player->Size.x / 2.0f);
    // then move accordingly
    float strength = 2.0f;
    glm::vec2 oldVelocity = Ball->Velocity;
    Ball->Velocity.x = INITIAL_BALL_VELOCITY.x * percentage * strength;
    // Ball->Velocity.y = -Ball->Velocity.y;
    Ball->Velocity =
        glm::normalize(Ball->Velocity) *
        glm::length(oldVelocity); // keep speed consistent over both axes
                                  // (multiply by length of old velocity, so
                                  // total strength is not changed)
    // fix sticky paddle
    Ball->Velocity.y = -1.0f * abs(Ball->Velocity.y);

    // if Sticky powerup is activated, also stick ball to paddle once new
    // velocity vectors were calculated
    Ball->Stuck = Ball->Sticky;

    SoundEngine->play2D("resources/audio/bleep.wav", false);
  }
}

bool CheckCollision(GameObject &one, GameObject &two) // AABB - AABB collision
{
  // collision x-axis?
  bool collisionX = one.Position.x + one.Size.x >= two.Position.x &&
                    two.Position.x + two.Size.x >= one.Position.x;
  // collision y-axis?
  bool collisionY = one.Position.y + one.Size.y >= two.Position.y &&
                    two.Position.y + two.Size.y >= one.Position.y;
  // collision only if on both axes
  return collisionX && collisionY;
}

Collision CheckCollision(BallObject &one,
                         GameObject &two) // AABB - Circle collision
{
  // get center point circle first
  glm::vec2 center(one.Position + one.Radius);
  // calculate AABB info (center, half-extents)
  glm::vec2 aabb_half_extents(two.Size.x / 2.0f, two.Size.y / 2.0f);
  glm::vec2 aabb_center(two.Position.x + aabb_half_extents.x,
                        two.Position.y + aabb_half_extents.y);
  // get difference vector between both centers
  glm::vec2 difference = center - aabb_center;
  glm::vec2 clamped =
      glm::clamp(difference, -aabb_half_extents, aabb_half_extents);
  // now that we know the clamped values, add this to AABB_center and we get the
  // value of box closest to circle
  glm::vec2 closest = aabb_center + clamped;
  // now retrieve vector between center circle and closest point AABB and check
  // if length < radius
  difference = closest - center;

  if (glm::length(difference) <
      one.Radius) // not <= since in that case a collision also occurs when
                  // object one exactly touches object two, which they are at
                  // the end of each collision resolution stage.
    return std::make_tuple(true, VectorDirection(difference), difference);
  else
    return std::make_tuple(false, UP, glm::vec2(0.0f, 0.0f));
}

// calculates which direction a vector is facing (N,E,S or W)
Direction VectorDirection(glm::vec2 target) {
  glm::vec2 compass[] = {
      glm::vec2(0.0f, 1.0f),  // up
      glm::vec2(1.0f, 0.0f),  // right
      glm::vec2(0.0f, -1.0f), // down
      glm::vec2(-1.0f, 0.0f)  // left
  };
  float max = 0.0f;
  unsigned int best_match = -1;
  for (unsigned int i = 0; i < 4; i++) {
    float dot_product = glm::dot(glm::normalize(target), compass[i]);
    if (dot_product > max) {
      max = dot_product;
      best_match = i;
    }
  }
  return (Direction)best_match;
}
